package cn.edu.sjtu.process_management.service;

import cn.edu.sjtu.process_management.constant.*;
import cn.edu.sjtu.process_management.exception.model.BpmnException;
import cn.edu.sjtu.process_management.exception.model.ServiceException;
import cn.edu.sjtu.process_management.model.History;
import cn.edu.sjtu.process_management.model.Log;
import cn.edu.sjtu.process_management.model.Result;
import cn.edu.sjtu.process_management.model.User;
import cn.edu.sjtu.process_management.repository.HistoryRepository;
import cn.edu.sjtu.process_management.util.StrConvertUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

import static org.apache.commons.lang3.math.NumberUtils.isCreatable;
import static org.apache.commons.lang3.math.NumberUtils.createDouble;

import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.camunda.bpm.model.bpmn.instance.*;
import org.camunda.bpm.model.bpmn.instance.camunda.CamundaProperties;
import org.camunda.bpm.model.bpmn.instance.camunda.CamundaProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author loumoon
 * @date 2019-11-15 10:20
 */
@Service
public class AsyncService {
    private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

    @Autowired
    private HistoryRepository historyRepository;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private MessageService messageService;

    @Value("${detect.polling.intervals}")
    private Integer detectPollingIntervals; // 轮询监控状态的时间间隔

    private static final Map<String, Boolean> BOOL_STRINGS = new HashMap<String, Boolean>() {{
        put("true", true);
        put("false", false);
    }};

    /*执行流程*/
    @Async("asyncServiceExecutor")
    public void executeProcess(History history, String userId) {
        BpmnModelInstance instance = Bpmn.readModelFromStream(StrConvertUtil.strToInStream(history.getBpmnInstance())); // 加载bpmn模型

        FlowNode node = instance.getModelElementsByType(StartEvent.class).iterator().next(); // 开始节点

        Map<String, Map<String, Object>> dataCache = new HashMap<>(); // 全局数据集

        while (true) {
            if (node instanceof StartEvent) {
                // 将执行的下一个节点
                //node = ((SequenceFlow) instance.getModelElementById(node.getUniqueChildElementByType(Outgoing.class).getTextContent()))
                //        .getTarget();
                node = node.getOutgoing().iterator().next().getTarget();
                // logger.info("startEvent");
            } else if (node instanceof Task) {
                String taskId = node.getId();
                // 取出Task的扩展属性
                Collection<CamundaProperty> properties = node.getExtensionElements().getElementsQuery()
                        .filterByType(CamundaProperties.class).singleResult().getCamundaProperties();
                Map<String, CamundaProperty> propertiesMap = properties.stream().collect(
                        Collectors.toMap(CamundaProperty::getCamundaName, Function.identity()));

                // 解析微服务的启动接口、监控状态接口
                String startInterface = propertiesMap.get("startInterface").getCamundaValue();
                String detectInterface = propertiesMap.get("detectInterface").getCamundaValue();

                // 解析用户选中的微服务实例
                Collection<CamundaProperty> instances = propertiesMap.get("instances").getChildElementsByType(CamundaProperty.class); // 所有微服务实例
                Collection<CamundaProperty> instanceUrls = propertiesMap.get("instanceUrls").getChildElementsByType(CamundaProperty.class);
                Map<String, String> instancesMap = instances.stream().collect(
                        Collectors.toMap(CamundaProperty::getCamundaName, CamundaProperty::getCamundaValue));
                Map<String, String> instanceUrlsMap = instanceUrls.stream().collect(
                        Collectors.toMap(CamundaProperty::getCamundaName, CamundaProperty::getCamundaValue));
                List<String> instancesChosen = instancesMap.entrySet().stream()
                        .filter(entry -> "true".equals(entry.getValue())) // 过滤选中的微服务实例
                        .map(Map.Entry::getKey) // 取微服务实例addr
                        .map(instanceUrlsMap::get)
                        .collect(Collectors.toList());
                // logger.info(JSON.toJSONString(instancesChosen));

                // 解析配置参数
                // TODO: redesign & adjust the extension property so that it contains argument type information
                Collection<CamundaProperty> arguments = propertiesMap.get("arguments").getChildElementsByType(CamundaProperty.class);
                Map<String, Object> argumentsMap = arguments.stream()
                        .map(camundaProperty -> {
                            String key = camundaProperty.getCamundaName();
                            String value = camundaProperty.getCamundaValue();
                            if (BOOL_STRINGS.keySet().contains(value)) {
                                return new AbstractMap.SimpleEntry<>(key, BOOL_STRINGS.get(value));
                            } else if (isCreatable(value)) { // is numeric
                                return new AbstractMap.SimpleEntry<>(key, createDouble(value));
                            } else {
                                return new AbstractMap.SimpleEntry<>(key, value);
                            }
                        })
                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
                // logger.info(JSON.toJSONString(argumentsMap));

                // 解析所需的其他task的输出数据data
                Collection<CamundaProperty> input = propertiesMap.get("input").getChildElementsByType(CamundaProperty.class);
                //Map<String, String> inputMap = input.stream().collect(
                //        Collectors.toMap(CamundaProperty::getCamundaName, CamundaProperty::getCamundaValue));
                // 从全局数据集中取出所需数据的值
                JSONObject data = new JSONObject();
                for (CamundaProperty inputData : input) {
                    String key = inputData.getCamundaName();    // key is the name of this parameter
                    String value = inputData.getCamundaValue(); // value is like `${taskId}.${outputValue}`
                    String inputTaskId = value.split("[.]")[0];
                    String inputTaskOutput = value.split("[.]")[1];
                    data.put(key, dataCache.get(inputTaskId).get(inputTaskOutput));
//                    data.put(inputData.getCamundaValue(), dataCache.get(inputData.getCamundaName()).get(inputData.getCamundaValue()));
                }

                // 构造请求体
                JSONObject requestBody = new JSONObject();
                requestBody.put("processId", history.getId()); // 流程运行记录 id
                requestBody.put("taskId", taskId); // task的id
                data.putAll(JSONObject.parseObject(JSON.toJSONString(argumentsMap))); // 合并配置参数到data，一起作为task的输入数据
                requestBody.put("data", data); // 输入数据


                // 选择一个微服务实例加入其待处理队列
                Iterator<String> iter = instancesChosen.iterator();
                String instanceJoin;
                while (true) {
                    if (iter.hasNext()) {
                        String ins = iter.next();
                        int status = statusOfECN(ins, detectInterface);
                        if (status == DeviceStatus.READY.ordinal()) { // ready的实例可以立即加入
                            instanceJoin = ins;
                            break;
                        }
                        if (status == DeviceStatus.ABNORMAL.ordinal()) { // abnormal的实例要从备选中删除
                            iter.remove();
                        }
                    } else { // 所有微服务实例状态均不为ready
                        instanceJoin = instancesChosen.get(new Random().nextInt(instancesChosen.size())); // 随机一个实例加入
                        break;
                    }
                }
                startECN(requestBody, history, taskId, instanceJoin, startInterface); // 加入选中微服务实例的待处理队列

                if (node instanceof UserTask) {
                    try {
                        User user = messageService.getUserByUserId(userId);
                        JSONArray toUser = messageService.getUserByECN(instanceJoin);
                        JSONObject message = new JSONObject();
                        message.put("content", "您有新任务，请及时处理");
                        messageService.sendMessage(user, toUser, message);
                    } catch (Exception e) {
                        logger.warn("got exception on notifying user task");
                    }
                }

                // 轮询微服务实例监控task的处理状态
                while (true) {
                    JSONObject detectData = new JSONObject(); // 检测到的数据
                    if (detectECN(detectData, history, taskId, instanceJoin, detectInterface)) { // 检测到的数据是当前task的处理状态
                        //logger.info(detectData.toJSONString());
                        dataCache.put(taskId, (Map<String, Object>) detectData.get("data")); // 更新全局数据集
                        if (detectData.get("status") != null) {
                            //logger.info("detectData status not null: " + (int) detectData.get("status"));
                            if ((int) detectData.get("status") == TaskStatus.DONE.ordinal()) { // task处理完成
                                break;
                            } else if ((int) detectData.get("status") == TaskStatus.ABNORMAL.ordinal()) {
                                throw new ServiceException(RespStatus.BPMN_PROCESS_EXECUTE_ERROR, this.getClass().getName());
                            }
                        }
                    }
                    try {
                        Thread.sleep(detectPollingIntervals); // 休眠
                    } catch (InterruptedException e) {
                        throw new ServiceException(RespStatus.SERVER_ERROR, this.getClass().getName());
                    }
                }

                // 将执行的下一个节点
                //node = ((SequenceFlow) instance.getModelElementById(node.getUniqueChildElementByType(Outgoing.class).getTextContent()))
                //        .getTarget();
                node = node.getOutgoing().iterator().next().getTarget();
            } else if (node instanceof ExclusiveGateway) {
                // 取出Gateway的扩展属性
                // logger.info("into gateway: " + node.getId());
                Collection<CamundaProperty> properties = node.getExtensionElements().getElementsQuery()
                        .filterByType(CamundaProperties.class).singleResult().getCamundaProperties();
                Map<String, CamundaProperty> propertiesMap = properties.stream().collect(
                        Collectors.toMap(CamundaProperty::getCamundaName, Function.identity()));
                //logger.info("dataCache: " + JSON.toJSONString(dataCache));

                // 条件判断结果
                boolean test = false;

                // 进行条件判断
                for (CamundaProperty judgement : propertiesMap.values()) {
                    Collection<CamundaProperty> components = judgement.getChildElementsByType(CamundaProperty.class);
                    Map<String, CamundaProperty> componentsMap = components.stream().collect(
                            Collectors.toMap(CamundaProperty::getCamundaName, Function.identity()));
                    /*
                     * edited by Cigui
                     * To support both literal and reference for left & right values, they should have fixed style like
                     * "lit: ${value}" or "ref: ${reference}".
                     */
                    String leftValue = componentsMap.get("left").getCamundaValue();
                    String rightValue = componentsMap.get("right").getCamundaValue();
                    String parsedLeftValue = parseExpressionValue(dataCache, leftValue);
                    String parsedRightValue = parseExpressionValue(dataCache, rightValue);
                    String comparisonOperator = componentsMap.get("middle").getCamundaValue();
                    boolean bothCreatable = isCreatable(parsedLeftValue) && isCreatable(parsedRightValue);
//                    logger.info("left: " + parsedLeftValue);
//                    logger.info("right: " + parsedRightValue);
//                    logger.info("creatable: " + bothCreatable);
//                    logger.info("comparison operator: " + comparisonOperator);
                    switch (ComparisonOperator.from(comparisonOperator)) {
                        case EQUAL:
                            test = bothCreatable
                                    ? createDouble(parsedLeftValue).equals(createDouble(parsedRightValue))
                                    : parsedLeftValue.equals(parsedRightValue);
                            break;
                        case LESSTHAN:
                            test = bothCreatable && createDouble(parsedLeftValue) < createDouble(parsedRightValue);
                            break;
                        case GREATERTHAN:
                            test = bothCreatable && createDouble(parsedLeftValue) > createDouble(parsedRightValue);
                            break;
                        case LESSEQEUAL:
                            test = bothCreatable && createDouble(parsedLeftValue) <= createDouble(parsedRightValue);
                            break;
                        case GREATEREQUAL:
                            test = bothCreatable && createDouble(parsedLeftValue) >= createDouble(parsedRightValue);
                            break;
                        default:    // not equal by default
                            test = bothCreatable
                                    ? (!createDouble(parsedLeftValue).equals(createDouble(parsedRightValue)))
                                    : (!parsedLeftValue.equals(parsedRightValue));
                    }
                }
                // 根据条件判断结果寻找待执行的下一个Task
                String finalResult = test ? "yes" : "no";
                //logger.info("final result: " + finalResult);
                Optional<SequenceFlow> choice = node.getOutgoing().stream()
                        .filter(sequenceFlow -> sequenceFlow.getName().equals(finalResult)).findAny();
                if (!choice.isPresent()) {
                    //logger.info("no choice is present!");
                    throw new BpmnException(RespStatus.BPMN_PROCESS_EXECUTE_ERROR);
                } else {
                    node = choice.get().getTarget();
                    //logger.info("next node: " + node.getId());
                }
//                List<Outgoing> outgoings = (ArrayList<Outgoing>) node.getChildElementsByType(Outgoing.class);
//                if (outgoings.size() != 2) { // 异或网关，只支持两条输出路径
//                    throw new BpmnException(RespStatus.BPMN_PROCESS_EXECUTE_ERROR);
//                }
//                for (int i = 0; i < outgoings.size(); ++i) { // 选择符合条件的输出路径
//                    SequenceFlow flow = instance.getModelElementById(outgoings.get(i).getTextContent());
//                    Collection<CamundaProperty> flowProperties = flow.getExtensionElements().getElementsQuery()
//                            .filterByType(CamundaProperties.class).singleResult().getCamundaProperties();
//                    Map<String, CamundaProperty> flowPropertiesMap = flowProperties.stream().collect(
//                            Collectors.toMap(CamundaProperty::getCamundaName, Function.identity()));
//                    if (result.equals(flowPropertiesMap.get("condition").getCamundaValue())) {
//                        node = flow.getTarget();
//                        break;
//                    } else {
//                        if (i == outgoings.size() - 1) { // 两条输出路径的条件都不符合
//                            throw new BpmnException(RespStatus.BPMN_PROCESS_EXECUTE_ERROR);
//                        }
//                    }
//                }
            } else if (node instanceof EndEvent) {
                history.setStatus(HistoryStatus.DONE.ordinal()); // 修改流程状态为完成
                history.setEnd(new Date()); // 保存执行完成时间
                historyRepository.save(history);

                //logger.info("endEvent");
                break; // 流程执行完毕，跳出循环
            } else {
                throw new BpmnException(RespStatus.BPMN_PROCESS_PARSE_ERROR);
            }
        }
        logger.info("end executeProcess");
    }

    /*启动ECN微服务实例执行task*/
    private void startECN(JSONObject requestBody, History history, String taskId, String instance, String startInterface) {
//        logger.info("starting ECN: taskId: " + taskId + ", instance: " + instance + ", startInterface: " + startInterface);
//        logger.info("post for url: " + instance + startInterface);
//        logger.info("request body: " + requestBody.toJSONString());
        ResponseEntity<Result> response = restTemplate.postForEntity(instance + startInterface, requestBody, Result.class);
        Result result = response.getBody();
        if (result == null) {
            throw new ServiceException(RespStatus.ERROR_RESPONSE_FROM_ECN, this.getClass().getName());
        }
        Log log = Log.builder()
                .time(new Date())
                .task(taskId)
                .instance(instance)
                .api(startInterface)
                .requestUrl(instance + startInterface)
                .requestBody(requestBody.toString())
                .responseCode(result.getStatus())
                .responseBody(null)
                .status(null)
                .build();
        history.getLogs().add(log);
        //logger.info("log appended");
        boolean isnull = historyRepository == null;
        //logger.info(isnull + "");
        historyRepository.save(history);    // update history logs
        if (!result.getStatus().equals(RespStatus.SUCCESS.getCode())) {
            //logger.info("RespStatus of start is not success!");
            history.setStatus(HistoryStatus.FAIL.ordinal());
            throw new ServiceException(RespStatus.FAIL_START_ECN, this.getClass().getName());
        }
        //logger.info("finish starting ecn");
    }

    private int statusOfECN(String instance, String interfaceUrl) {
        ResponseEntity<Result> response = restTemplate.getForEntity(instance + interfaceUrl, Result.class);
        Result result = response.getBody();
        if (result == null) {
            //logger.info("result is null!");
            throw new ServiceException(RespStatus.ERROR_RESPONSE_FROM_ECN, this.getClass().getName());
        }
        JSONObject data = JSONObject.parseObject(JSON.toJSONString(result.getData()));
        return (int) data.get("status");
    }

    /*监控ECN微服务实例的状态，返回实例是否正在处理指定task*/
    private boolean detectECN(JSONObject data, History history, String taskId, String instance, String detectInterface) {
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(instance + detectInterface)
                .queryParam("processId", history.getId())
                .queryParam("taskId", taskId);
        String url = builder.toUriString();
        //logger.info("detect url: " + url);
        ResponseEntity<Result> response = restTemplate.getForEntity(url, Result.class);
        Result result = response.getBody();
        //logger.info("result: " + JSON.toJSONString(result));
        if (result == null) {
            //logger.info("result is null!");
            throw new ServiceException(RespStatus.ERROR_RESPONSE_FROM_ECN, this.getClass().getName());
        }
        if (!result.getStatus().equals(RespStatus.SUCCESS.getCode())) {
            history.setStatus(HistoryStatus.FAIL.ordinal());
            throw new ServiceException(RespStatus.FAIL_DETECT_ECN, this.getClass().getName());
        } else {
            String json = JSON.toJSONString(result.getData());
            //logger.info("data: " + json);
            data.putAll(JSONObject.parseObject(json)); // '.putAll()'-Yes***'='-No
            // data.putAll((JSONObject) result.getData());
            TaskStatus status = TaskStatus.fromInteger((int) data.get("status"));
            Object dataInData = data.get("data");

            switch (status) {
                case EXECUTING:
                case DONE:
                    data.put("status", status.ordinal()); // 更改状态
                    Log log = Log.builder()
                            .time(new Date())
                            .task(taskId)
                            .instance(instance)
                            .api(detectInterface)
                            .requestUrl(instance + detectInterface)
                            .requestBody(null)
                            .responseCode(result.getStatus())
                            .responseBody(dataInData == null ? null : dataInData.toString())
                            .status((int) data.get("status"))
                            .build();
                    history.getLogs().add(log);
                    historyRepository.save(history);    // update history logs
                    return true;
                case ABNORMAL:
                    data.put("status", status.ordinal()); // 更改状态
                    return true;
                default:
                    return false;
            }
        }
    }

    private String parseExpressionValue(Map<String, Map<String, Object>> dataCache, String value) {
        if (value.startsWith("ref: ")) {
            String[] split = value.substring(5).split("[.]"); // reference value should be like `ref: ${taskId}.${outputValue}`
            String task = split[0];
            String key = split[1];
            return dataCache.get(task).get(key).toString();
        }
        // literal by default
        return value.substring(5);
    }
}
